<?php
/**
 * This file is part of DomLocker.
 * 
 * DomLocker is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * DomLocker is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with DomLocker.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @link http://inditel.ee/
 * @copyright 2007-2009 Inditel Meedia OÜ
 * @author Oliver Leisalu <oliver@inditel.ee>
 */

/**
* Helps to build locking mechanisms.
* 
* This class must work without any external files needed to be loaded. Any future development should consider this.
* 
* Basic usage
$locker = new DomLocker('myLockerName');
$locker->setWaitTime( 4 ); // how long to wait for maximum (seconds)
$locker->setResetTime( 30 ); // how long lock file is considered as valid. (seconds)
if( $locker->isLocked() ) {
	$locker->wait();	
} else {
	$locker->lock();
	// do something expensive that no one else should do on the same time.
	$locker->unlock();
}
* 
* @package DomLocker
*/
class DomLocker {
	
	/**
	 * @var String
	 */
	private $id;
	/**
	 * @var String
	 */
	private $info;
	/**
	 * @var Int
	 */
	private $waitTime = 10;
	/**
	 * @var Int
	 */
	private $resetTime = 60;
	
	/**
	 * @param Int $id
	 */
	public function __construct( $id  ) {
		$this->id = $id;
	}
	/**
	 * @param Int $id
	 */
	public function setWaitTime( $time ) {
		$this->waitTime = (int)$time;
	}
	/**
	 * @param Int $id
	 */
	public function setResetTime( $time ) {
		$this->resetTime = (int)$time;
	}
	/**
	 * This info will be written into the lock file. Useful for debugging.
	 * @param String $info
	 */
	public function setInfo( $info ) {
		$this->info = $info;
	}
	/**
	 * @return String
	 */
	public function getLockFilePath() {
		return DOM_CACHE_PATH . 'DomLocker/' . md5($this->id) .'.lock';
	}
	/**
	 * Waits until lock is released. Maximum time waited can be set by setWaitTime() method.
	 * 
	 * @return Bool
	 */
	public function wait() {
		
		if( $this->waitTime <= 0 )
			throw new RuntimeException('Can\t wait for lock, waitTime is '.$this->waitTime.' <= 0');
		
		$loopCounter = 0;
		while( $this->isLocked() === true ) {
			$sleep = round( mt_rand(40,60) * 10000);
			$loopCounter += $sleep/1000000;
			usleep( $sleep );
			if( $loopCounter >= $this->waitTime ) {
				
				$message = 'Cache file is locked for more than '.$loopCounter.' seconds! <br />';
				$message .= '<b>Cache id: </b>'.$this->id.'<br />';
				$message .= '<b>Lock file created: </b>'.(microtime(true)-filemtime($this->getLockFilePath())).' seconds ago.<br />';
				$message .= '<b>Request info: </b> '.$this->info.'<br />';
				try {
					$contents = explode("\n\r", file_get_contents( $this->getLockFilePath() ) );
				} catch (Exception $e ) {
					return;
				}
				$message .= '<b>Lock owner info: </b>'.$contents[1];
				
				throw new LockTimeExceededException( $message );
			}
		}
	}
	/**
	 * Acquires lock. If lock file exists, returns false.
	 *
	 * @return Bool
	 */
	public function lock() {
		$file = $this->getLockFilePath();
		
		$dir = dirname( $file );
		if( is_dir( $dir ) === false ) {
			mkdir( $dir );
		}
		
		// test if lock is already taken. (also checks if lock has timed out)
		if( $this->isLocked() === true ) {
			return false;
		}
			
		try {
			$f = fopen($file, 'x');
			if( !$f ) {
				return false;
			}
		} catch ( Exception $e ) {
			return false;
		}
		fwrite($f, $this->id."\n\r".$this->info);
		fclose($f);
		return true;
	}
	/*
	 * Removes lock
	 * 
	 * @return Bool
	 */
	public function unlock() {
		$file  = $this->getLockFilePath();
		try {
			
			if( file_exists($file) ) {
				unlink( $file ); 
				return true;
			
			} else {
				return false;
			}
			
		} catch( Exception $e ) {
			return false;
		}
	}
	/**
	 * Tests if lock is on
	 *
	 * @return Bool true if lock is on, false - if off.
	 */
	public function isLocked() {
		
		$file = $this->getLockFilePath();
		if( file_exists( $file ) === true ) {
			try {
				if( $this->resetTime > 0 && filemtime($file) <= time()-$this->resetTime ) {
					@unlink($file);
					return false;
				
				} else {
					return true;
				}
			// Exception can only come when filemtime fails and that happens if cache file is deleted, so cache is not locked.
			} catch( Exception $e ) {
				return false;
			}
		}
		
		return false;
	}
}

/**
 * @package DomLocker
 *
 */
class LockTimeExceededException extends RuntimeException {
	
	
}
?>